#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@Author          : ricsy
@File            : sphinx_directory_tree.py
@Create Time     : 2025/6/8 06:02
@Last Modified   :
@Description     : 目录树插件
"""
import fnmatch
import json
import os
import re
from pathlib import Path
from typing import Dict, List, Optional, Tuple

from docutils import nodes
from docutils.parsers.rst import Directive
from pydantic import (
    BaseModel,
    Field,
    field_validator,
)

from config.icons import DEFAULT_FILE_ICONS, FILE_ICONS
from extensions.common import deep_merge
from src.settings import logger


DEFAULT_DEPTH_LIMIT = 10
MAX_PATH_LENGTH = 4096  # 大多数 Unix 系统的路径长度限制

# 颜色名称到 ANSI 代码的映射
COLOR_MAP = {
    # 基础颜色
    "black": "\033[30m",
    "red": "\033[31m",
    "green": "\033[32m",
    "yellow": "\033[33m",
    "blue": "\033[34m",
    "magenta": "\033[35m",
    "cyan": "\033[36m",
    "white": "\033[37m",
    "gray": "\033[90m",  # 添加 gray 作为 bright_black 的别名
    "grey": "\033[90m",  # gray 的英式拼写
    "light_gray": "\033[37m",
    "dark_gray": "\033[90m",
    # 亮色
    "bright_black": "\033[90m",
    "bright_red": "\033[91m",
    "bright_green": "\033[92m",
    "bright_yellow": "\033[93m",
    "bright_blue": "\033[94m",
    "bright_magenta": "\033[95m",
    "bright_cyan": "\033[96m",
    "bright_white": "\033[97m",
    # 背景色
    "bg_black": "\033[40m",
    "bg_red": "\033[41m",
    "bg_green": "\033[42m",
    "bg_yellow": "\033[43m",
    "bg_blue": "\033[44m",
    "bg_magenta": "\033[45m",
    "bg_cyan": "\033[46m",
    "bg_white": "\033[47m",
    # 样式
    "bold": "\033[1m",  # 加粗
    "dim": "\033[2m",  # 暗淡/弱化
    "italic": "\033[3m",  # 斜体
    "underline": "\033[4m",  # 下划线
    "blink": "\033[5m",  # 闪烁
    "reverse": "\033[7m",  # 反转/高亮
    "hidden": "\033[8m",  # 隐藏
    # 重置
    "reset": "\033[0m",
}


def get_file_icon(filename: str, return_type: str = "symbol") -> Tuple[str, str]:
    """
    获取文件图标
    :param filename: 文件名
    :param return_type: 返回类型，symbol(Unicode 符号) 或 element(HTML 元素)
    :return: 图标符号或类名
    """
    element_type = "element"
    symbol_type = "symbol"

    # 获取文件后缀（转换为小写）
    ext = os.path.splitext(filename)[1].lower()
    icon_data = FILE_ICONS.get(ext, DEFAULT_FILE_ICONS)

    return_type = return_type.lower()
    icon = icon_data.get(symbol_type, "")
    icon_element = icon_data.get(element_type, "")
    if return_type == element_type and icon_element:
        icon = icon_element
    else:
        return_type = symbol_type
    return icon, return_type


# noinspection PyMethodParameters
class DirectoryTreeConfig(BaseModel):
    """目录树显示配置"""

    symbols: Dict[str, str] = Field(
        default={
            "root": "📌",  # 根目录图标
            "dir": "🗂️",  # 目录图标
            "file": "📄",  # 文件图标
            "vertical": "│",  # 垂直连接符
            "branch": "├",  # 分支连接符
            "end": "└",  # 末端连接符
            "horizontal": "──",  # 水平连接符
            "space": "    ",  # 缩进空格
        },
        description="显示符号",
    )
    colors: Dict[str, str] = Field(
        default={
            "root": "bold",  # 根目录颜色
            "dir": "blue",  # 目录颜色
            "file": "green",  # 文件颜色
            "connector": "gray",  # 连接符颜色
            "reset": "reset",  # 重置颜色
        },
        description="颜色配置",
    )
    display: Dict[str, bool] = Field(
        default={
            "show_icon": True,  # 显示图标
            "colorful": True,  # 彩色显示
            "show_hidden": False,  # 显示隐藏文件
        },
        description="显示选项",
    )

    @field_validator("symbols", "colors", "display")
    def validate_dict_not_empty(cls, v):
        if not v:
            raise ValueError("配置字典不能为空")
        return v


# noinspection PyMethodParameters
class DirectoryTreeParams(BaseModel):
    """目录树生成参数"""

    path: str = Field(default=".", description="要扫描的根目录路径")
    max_depth: int = Field(
        default=2,
        gt=0,
        le=DEFAULT_DEPTH_LIMIT,
        description=f"递归深度",
    )
    exclude: List[str] = Field(default_factory=list, description="要排除的文件模式列表")
    config: DirectoryTreeConfig = Field(
        default_factory=DirectoryTreeConfig, description="显示配置"
    )
    output_format: str = Field(default="text", description="输出格式(text/html)")

    @field_validator("path")
    def validate_path_exists(cls, v):
        if not isinstance(v, str):
            raise TypeError(f"path 必须是字符串，得到 {type(v)}")
        if len(v) > MAX_PATH_LENGTH:
            raise ValueError(f"路径长度超过 {MAX_PATH_LENGTH} 字符限制")

        path = Path(v)
        if not path.exists():
            raise FileNotFoundError(f"路径不存在: {v}")
        if not path.is_dir():
            raise NotADirectoryError(f"不是目录: {v}")
        return str(path.resolve())

    @field_validator("exclude")
    def validate_exclude_patterns(cls, v):
        """验证每个排除模式是否有效"""
        if not isinstance(v, list):
            raise TypeError("exclude 必须是列表")

        validated = []
        for pattern in v:
            if not isinstance(pattern, str):
                raise TypeError("排除模式必须是字符串")
            try:
                re.compile(fnmatch.translate(pattern))
                validated.append(pattern)
            except re.error as e:
                raise ValueError(f"无效的通配符模式: {pattern} ({str(e)})")
        return validated


def resolve_color(color_name: Optional[str]) -> str | None:
    """
    将颜色名称解析为 ANSI 代码
    :param color_name: 颜色名称或直接的 ANSI 代码
    :return: ANSI 颜色代码字符串
    """
    color_ansi = None
    if color_name:
        ansi_codes = []
        for part in color_name.lower().split():
            code = COLOR_MAP.get(part)
            if code:
                ansi_codes.append(code)
            else:
                logger.warning(f"不支持的颜色名称部分: '{part}'")
        color_ansi = "".join(ansi_codes) if ansi_codes else None
    if not color_ansi:
        logger.warning(f"不支持颜色名称: '{color_name}'")
    return color_ansi


def _generate_text_tree(
    path: str, max_depth: int, exclude: List[str], cfg: dict
) -> str:
    """
    生成带颜色映射的目录树
    :param path:
    :param max_depth:
    :param exclude:
    :param cfg:
    :return: 格式化后的目录树字符串
    """
    sym = cfg["symbols"]
    disp = cfg["display"]

    # 解析颜色配置
    colors = {
        key: resolve_color(value)
        for key, value in cfg["colors"].items()
        if resolve_color(value)
    }

    def build_tree(current_path: str, current_depth: int, prefix: str) -> List[str]:
        """递归生成目录树"""
        lines = []
        entries = []

        try:
            with os.scandir(current_path) as it:
                for entry in it:
                    if not disp["show_hidden"] and entry.name.startswith("."):
                        continue
                    if any(exc in entry.path for exc in exclude):
                        continue
                    entries.append(entry)

                # 按目录在前，文件在后排序，同类型按名称升序排序
                entries.sort(key=lambda x: (not x.is_dir(), x.name.lower()))

                # 当前条目显示
                for i, entry in enumerate(entries):
                    # 是否为最后一个条目
                    is_last_entry = i == len(entries) - 1

                    # 连接线
                    if current_depth == 0:
                        connector = ""
                    else:
                        connector = (
                            f"{sym['end']}{sym['horizontal']}"
                            if is_last_entry
                            else f"{sym['branch']}{sym['horizontal']}"
                        )
                        if disp["colorful"]:
                            connector = (
                                f"{colors['connector']}{connector}{colors['reset']}"
                            )

                    # 行前缀
                    line_prefix = prefix + connector

                    # 子前缀
                    child_prefix = prefix + (
                        sym["space"] if is_last_entry else f"{sym['vertical']}   "
                    )

                    # 条目显示
                    if entry.is_dir():
                        icon = sym["dir"] if disp["show_icon"] else ""
                        name = f"{colors['dir']}{entry.name}/{colors['reset']}"
                    else:
                        icon = get_file_icon(entry.name)[0] if disp["show_icon"] else ""
                        name = f"{colors['file']}{entry.name}{colors['reset']}"

                    lines.append(f"{line_prefix}{icon}{name}")

                    # 递归处理子目录
                    if entry.is_dir() and current_depth < max_depth - 1:
                        lines.extend(
                            build_tree(
                                entry.path,
                                current_depth + 1,
                                child_prefix,
                            )
                        )

        except Exception as e:
            return [f"无法访问: {current_path} ({str(e)})"]

        return lines

    # 根目录显示
    root_name = os.path.basename(os.path.abspath(path))
    root_line = f"{sym['root'] if disp['show_icon'] else ''}{colors['root']}{root_name}{colors['reset']}"
    return "\n".join([root_line] + build_tree(path, 0, ""))


def _should_exclude(
    entry: os.DirEntry, exclude: List[str], disp: Dict[str, bool]
) -> bool:
    """
    高级排除逻辑，支持：
    - 隐藏文件过滤
    - 通配符模式
    - 路径匹配
    - 性能优化
    """
    # 快速返回无需检查的情况
    if not exclude and disp["show_hidden"]:
        return False

    # 处理隐藏文件
    if not disp["show_hidden"] and entry.name.startswith("."):
        return True

    # 没有排除模式时直接返回
    if not exclude:
        return False

    # 预处理路径（性能优化）
    normalized_path = entry.path.replace("\\", "/")
    name = entry.name

    # 检查所有排除模式
    return any(
        fnmatch.fnmatchcase(name, pattern)
        or fnmatch.fnmatchcase(normalized_path, pattern)
        for pattern in exclude
    )


def _generate_html_tree(
    path: str, max_depth: int, exclude: List[str], cfg: dict
) -> str:
    """生成带 CSS 样式的 HTML"""
    sym = cfg["symbols"]
    disp = cfg["display"]

    # 使用两个列表分别存储标题和内容
    title_lines = []
    content_lines = []

    def build_tree(current_path: str, current_depth: int, prefix_parts: List[str]):
        try:
            with os.scandir(current_path) as it:
                entries = sorted(
                    [e for e in it if not _should_exclude(e, exclude, disp)],
                    key=lambda x: (not x.is_dir(), x.name.lower()),
                )

                for i, entry in enumerate(entries):
                    is_last = i == len(entries) - 1

                    # 构建连接线
                    connector = ""
                    if current_depth > 0:
                        connector = (
                            f'<span class="connector">'
                            f'{"└" if is_last else "├"}──'
                            f"</span>"
                        )

                    # 构建条目
                    icon = ""
                    if entry.is_dir():
                        icon = sym["dir"]
                    elif disp["show_icon"]:
                        icon_element, icon_type = get_file_icon(entry.name, "element")
                        icon = (
                            f"&thinsp;{icon_element}"
                            if icon_type == "element"
                            else icon_element
                        )

                    name = entry.name + ("/" if entry.is_dir() else "")
                    class_name = "dir" if entry.is_dir() else "file"

                    line = (
                        f'<div class="tree-line">'
                        f'{"".join(prefix_parts)}'
                        f"{connector}"
                        f'<span class="{class_name}">{icon}&thinsp;{name}</span>'
                        f"</div>"
                    )
                    content_lines.append(line)

                    # 递归处理子目录
                    if entry.is_dir() and current_depth < max_depth - 1:
                        new_prefix = prefix_parts + [
                            f'<span class="connector">{"    " if is_last else "│   "}</span>'
                        ]
                        build_tree(entry.path, current_depth + 1, new_prefix)

        except Exception as e:
            content_lines.append(
                f'<div class="error">无法访问: {current_path} ({str(e)})</div>'
            )

    # 根目录显示
    root_name = os.path.basename(os.path.abspath(path))
    root_icon = sym["root"] if disp["show_icon"] else ""
    title_lines.append(f'<div class="root-entry">{root_icon}{root_name}</div>')

    # 构建目录树内容
    build_tree(path, 0, [])

    # 组合最终HTML结构
    return (
        f'<div class="directory-tree">'
        f'{"".join(title_lines)}'
        f'<div class="directory-content">{"".join(content_lines)}</div>'
        f"</div>"
    )


def generate_directory_tree(**kwargs) -> str:
    """
    生成目录树，支持HTML和纯文本格式
    """
    # 参数校验
    params = DirectoryTreeParams(**kwargs)

    config = params.config.model_dump()
    output_format = params.output_format

    # 深度合并配置
    cfg = deep_merge(DirectoryTreeConfig().model_dump(), config or {})
    if output_format == "html":
        cfg["display"]["colorful"] = False

    if params.output_format == "html":
        return _generate_html_tree(params.path, params.max_depth, params.exclude, cfg)
    return _generate_text_tree(params.path, params.max_depth, params.exclude, cfg)


def _validate_config(arg: str) -> Dict:
    """安全JSON解析"""
    if not arg.strip():
        return {}
    try:
        config = json.loads(arg)
        if not isinstance(config, dict):
            raise ValueError("Config must be a dictionary")
        return config
    except json.JSONDecodeError as e:
        raise ValueError(f"Invalid JSON config: {str(e)}")


class TreeDirective(Directive):
    required_arguments = 1
    option_spec = {  # 直接使用模型字段的类型注解
        "path": DirectoryTreeParams.model_fields["path"].annotation,
        "maxdepth": DirectoryTreeParams.model_fields["max_depth"].annotation,
        "exclude": lambda x: x.split(),  # 将字符串转换为列表
        "config": _validate_config,  # 安全解析字典
        "format": lambda x: x.lower() if x.lower() in ("text", "html") else "html",
    }

    @staticmethod
    def is_html_output(settings) -> bool:
        """判断当前是否生成HTML输出"""
        # 方法1：检查是否存在builder属性且格式为html
        if (
            hasattr(settings, "builder")
            and getattr(settings.builder, "format", "") == "html"
        ):
            return True

        # 方法2：检查输出文件名后缀
        if hasattr(settings, "_destination") and str(
            getattr(settings, "_destination")
        ).endswith(".html"):
            return True

        # 方法3：检查是否存在HTML特定的设置
        if getattr(settings, "embed_stylesheet", False) or getattr(
            settings, "cloak_email_addresses", False
        ):
            return True

        return False

    def run(self):
        # 获取当前 builder 是否支持 HTML
        is_html = self.is_html_output(getattr(self.state.document, "settings", None))

        path = self.arguments[0]
        max_depth = self.options.get("maxdepth", 2)
        exclude = self.options.get("exclude", [])
        config = self.options.get("config", {})

        params = {
            "path": path,
            "max_depth": max_depth,
            "exclude": exclude,
            "config": config,
            "output_format": self.options.get("format", "html" if is_html else "text"),
        }

        try:
            tree = generate_directory_tree(**params)
            if params["output_format"] == "html":
                return [nodes.raw("", tree, format="html")]
            return [nodes.literal_block(text=tree)]
        except Exception as e:
            raise self.error(f"生成目录树失败: {str(e)}")


def setup(app):
    # 注册指令
    app.add_directive("dirtree", TreeDirective)
    # 添加 CSS 文件
    app.add_css_file("extensions/css/directory-tree.css")

    # 返回 Sphinx 扩展契约信息
    return {"version": "1.0", "parallel_read_safe": True, "parallel_write_safe": True}
